﻿using System;
using System.Collections.Generic;
using System.Text;

namespace TrackType
{

    /// <summary>
    /// Represents state of AlbumParser
    /// </summary>
    public enum AlbumParserState
    {
        /// <summary>
        /// Unknown parser state. 
        /// </summary>
        UnknownState = 0,
        /// <summary>
        /// Is reached if parser has successfully parsed album contents 
        /// </summary>
        FinalState = 1,
        /// <summary>
        /// Error state. Invalid input data
        /// </summary>
        SyntaxErrorState = 2,
        /// <summary>
        /// Is reached if syntax error has been occured or after FinalState
        /// </summary>
        EndState = 3,
        /// <summary>
        /// Initial parser state
        /// </summary>
        InitState = 4,
        /// <summary>
        /// State before reading album header
        /// </summary>
        ReadHeaderState = 5,
        /// <summary>
        /// State before reading frame offsets for album tracks
        /// </summary>
        ReadFrameOffsetsState = 6,
        /// <summary>
        /// State before reading disk length in frames
        /// </summary>
        ReadDiscLengthState = 7,
        /// <summary>
        /// State before reading disk attributes
        /// </summary>
        ReadDiscBaseAttributes = 8,
        /// <summary>
        /// State before reading track names
        /// </summary>
        ReadTracksNamesState = 9
    }

    /// <summary>
    /// Represents class used for parsing album contents from string array or string list
    /// </summary>
    public class AlbumParser
    {
        IList<string> lines;
        AlbumParserState currentState, previousState;
        int lineIndex, linesCount;
        string cmdCode;
        string cmdGenre;
        string cmdDiscID;
        long[] trackFrames;
        string[] trackNames;
        long discLength; // in seconds
        string artistName;
        string albumName;
        int albumYear;
        string albumGenre;

        #region Constants
        const int HeaderItemsLength = 3;
        const string ExactMatchCode = "210";
        const string InExactMatchCode = "211";
        const string FrameOffsetsMarker = "# Track frame offsets:";
        const string DelimeterString = "#";
        const string DiscLenPrefix = "# Disc length:";
        const int DiscLenIndex = 3;
        const string DiscIDPrefix = "DISCID=";
        const string DiscTitlePrefix = "DTITLE=";
        const string DiscYearPrefix = "DYEAR=";
        const string DiscGenrePrefix = "DGENRE=";
        const string ExtDPrefix = "EXTD=";
        const string TitlePrefix = "TTITLE";
        #endregion

        /// <summary>
        /// Gets current state of parser
        /// </summary>
        public AlbumParserState CurrentState
        {
            get
            {
                return currentState;
            }
        }
        /// <summary>
        /// Gets previous state of parsert
        /// </summary>
        public AlbumParserState PreviousState
        {
            get
            {
                return previousState;
            }
        }
        /// <summary>
        /// Gets command code (code of response from tracktype site)
        /// </summary>
        public string CommandCode
        {
            get
            {
                return cmdCode;
            }
        }
        /// <summary>
        /// Gets genre of album
        /// </summary>
        public string Genre
        {
            get
            {
                return albumGenre;
            }
        }
        /// <summary>
        /// Gets album unique identifier
        /// </summary>
        public string DiscID
        {
            get
            {
                return cmdDiscID;
            }
        }
        /// <summary>
        /// Gets disk length in seconds
        /// </summary>
        public long DiscLength 
        {
            get
            {
                return discLength;
            }
        }
        /// <summary>
        /// Gets artist name 
        /// </summary>
        public string ArtistName
        {
            get
            {
                return artistName;
            }
        }
        /// <summary>
        /// Gets album name
        /// </summary>
        public string AlbumName
        {
            get
            {
                return albumName;
            }
        }
        /// <summary>
        /// Gets album year
        /// </summary>
        public int Year
        {
            get
            {
                return albumYear;
            }
        }
        /// <summary>
        /// Gets tracks names
        /// </summary>
        public string[] TracksNames
        {
            get
            {
                return trackNames;
            }
        }
        /// <summary>
        /// Gets tracks frame offsets in CUE-sheet
        /// </summary>
        public long[] TrackFrames
        {
            get
            {
                return trackFrames;
            }
        }

        /// <summary>
        /// Creates new instance of parser using given list of text lines
        /// </summary>
        /// <param name="lines"></param>
        /// <exception cref="System.NullReferenceException">If lines parameter is null-refernce</exception>
        public AlbumParser(IList<string> lines)
        {
            if (lines == null)
                throw new NullReferenceException();

            this.lines = lines;
        }

        /// <summary>
        /// Attempts to parse album
        /// </summary>
        /// <returns>True if succeded, false otherwise</returns>
        public bool Parse()
        {
            bool result = false;

            SetInitState();
            Initialize();

            while (!IsStopState() && lineIndex < linesCount)
            {
                switch (CurrentState)
                {
                    case AlbumParserState.InitState:
                        break;
                    case AlbumParserState.FinalState:
                        {
                            result = true;
                            SetState(AlbumParserState.EndState);
                        }
                        break;
                    case AlbumParserState.SyntaxErrorState:
                        {
                            result = false;
                            SetState(AlbumParserState.EndState);
                        }
                        break;
                    case AlbumParserState.EndState:
                        break;
                    case AlbumParserState.ReadHeaderState:
                        {
                            if (ParseHeader(GetCurrentLine()))
                            {
                                SetState(AlbumParserState.ReadFrameOffsetsState);
                            }
                            else
                                SyntaxError();
                        }
                        break;
                    case AlbumParserState.ReadFrameOffsetsState:
                        {
                            List<string> frameLines = ParseTrackFrames();
                            trackFrames = ExtractFrameOffsets(frameLines);

                            if (trackFrames != null)
                            {
                                SetState(AlbumParserState.ReadDiscLengthState);
                            }
                            else
                                SyntaxError();
                        }
                        break;
                    case AlbumParserState.ReadDiscLengthState:
                        {
                            string str = GetCurrentLine();
                            discLength = ParseDiscLength(str);

                            if (discLength != -1)
                            {
                                SetState(AlbumParserState.ReadDiscBaseAttributes);
                            }
                            else
                                SyntaxError();
                        }
                        break;
                    case AlbumParserState.ReadDiscBaseAttributes:
                        {
                            if (ParseDiscBaseAttributes())
                            {
                                SetState(AlbumParserState.ReadTracksNamesState);
                            }
                            else
                                SyntaxError();
                        }
                        break;
                    case AlbumParserState.ReadTracksNamesState:
                        {
                            if (ParseTrackNamesList())
                            {
                                SetState(AlbumParserState.FinalState); // all work is done
                            }
                            else
                                SyntaxError();
                        }
                        break;
                }

                ++lineIndex;
            }

            return result;
        }

        /// <summary>
        /// Attempts to parse track names
        /// </summary>
        /// <returns>True if succeded, false otherwise</returns>
        protected bool ParseTrackNamesList()
        {
            List<string> trackNamesList = new List<string>();
            bool all_tracks_read = false;
            bool operation_succeded = false;

            while (lineIndex < linesCount)
            {
                string str = GetCurrentLine();
                if (str.StartsWith(ExtDPrefix))
                {
                    //end reading tracks names
                    all_tracks_read = true;
                    break; // while loop
                }
                else // string must contain TTITLEX[X] substring
                {
                    if (str.StartsWith(TitlePrefix))
                    {
                        string[] words = str.Split('=');
                        if (words.Length == 2)
                        {
                            trackNamesList.Add(words[1]);
                        }
                    }
                    else
                    {
                        SyntaxError();
                    }
                }
                ++lineIndex;
            }

            if (all_tracks_read)
            {
                /* Check whether parsed track names count
                 * mathes track frames count */
                if (trackNamesList.Count == trackFrames.Length)
                {
                    trackNames = trackNamesList.ToArray();
                    operation_succeded = true;
                }
                else
                    SyntaxError();
            }
            else
                SyntaxError();

            return operation_succeded;
        }
        /// <summary>
        /// Attempts to parse disk base attributes
        /// </summary>
        /// <returns>True if succede, false otherwise</returns>
        protected bool ParseDiscBaseAttributes()
        {
            bool discid_f = false,
                dtitle_f = false;

            string attribute_value = null;

            attribute_value = ExtractPrefixValue(DiscIDPrefix);
            if (String.Compare(attribute_value, cmdDiscID, StringComparison.InvariantCulture) == 0)
            {
                discid_f = true;
            }

            if (discid_f)
            {
                attribute_value = ExtractPrefixValue(DiscTitlePrefix);
                if (!String.IsNullOrEmpty(attribute_value))
                {
                    if (ExtractTitle(attribute_value, out artistName, out albumName))
                        dtitle_f = true;
                }
            }

            if (dtitle_f)
            {
                // year is optional
                attribute_value = ExtractPrefixValue(DiscYearPrefix);
                if (!String.IsNullOrEmpty(attribute_value))
                {
                    if (!ExtractYear(attribute_value, out albumYear))
                        albumYear = 0;
                }

                //genre is optional
                attribute_value = ExtractPrefixValue(DiscGenrePrefix);
                if (!String.IsNullOrEmpty(attribute_value))
                {
                    albumGenre = attribute_value;
                }
            }


            return dtitle_f && discid_f;
        }
        /// <summary>
        /// Gets current line parser is working at the moment
        /// </summary>
        /// <returns></returns>
        protected string GetCurrentLine()
        {
            return lines[lineIndex];
        }
        /// <summary>
        /// Sets new parser state and remember previous one
        /// </summary>
        /// <param name="newState">New state of parser</param>
        /// <returns>True if succeded, false otherwise</returns>
        protected bool SetState(AlbumParserState newState)
        {
            previousState = currentState;
            currentState = newState;

            return true;
        }
        /// <summary>
        /// Sets initial parser's state
        /// </summary>
        protected void SetInitState()
        {
            SetState(AlbumParserState.InitState);
        }
        /// <summary>
        /// Gets value indicating whether parser should stop it's work
        /// </summary>
        /// <returns>True is should stop, false otherwise</returns>
        protected bool IsStopState()
        {
            if (CurrentState == AlbumParserState.EndState)
                return true;
            else
                return false;
        }
        /// <summary>
        /// Executes syntax error parser command
        /// </summary>
        protected void SyntaxError()
        {
            SetState(AlbumParserState.SyntaxErrorState);
        }
        /// <summary>
        /// Initializes parser
        /// </summary>
        protected void Initialize()
        {
            lineIndex = 0;
            linesCount = lines.Count;

            if (linesCount == 0)
                SetState(AlbumParserState.SyntaxErrorState);
            else
                SetState(AlbumParserState.ReadHeaderState);
        }
        /// <summary>
        /// Attempts to parser album header
        /// </summary>
        /// <param name="str"></param>
        /// <returns>True if succeded, false otherwise</returns>
        protected bool ParseHeader(string str)
        {
            bool result = false;

            string[] words = str.Split(' ');

            if (words != null && words.Length == HeaderItemsLength)
            {
                try
                {
                    cmdCode = words[0];
                    cmdGenre = words[1];
                    cmdDiscID = words[2];

                    if (cmdCode == ExactMatchCode || cmdCode == InExactMatchCode)
                    {
                        if (!(String.IsNullOrEmpty(cmdGenre) || String.IsNullOrEmpty(cmdDiscID)))
                        {
                            result = true;
                        }
                    }
                }
                catch { }
            }

            return result;
        }
        /// <summary>
        /// Attempts to get string representation of tracks frame offsets in CUE-sheet
        /// </summary>
        /// <returns>List of string frames if succeded, null-reference otherwise</returns>
        protected List<string> ParseTrackFrames()
        {
            List<string> frameLines = null;

            bool foundStart = false, foundEnd = false;
            int startIndex = -1, endIndex = -1;

            while (lineIndex < linesCount) // find frames offset start
            {
                string str = GetCurrentLine();

                if (str.Contains(FrameOffsetsMarker))
                {
                    foundStart = true;
                    break;
                }

                ++lineIndex;
            }

            if (foundStart)
            {
                startIndex = ++lineIndex;

                //find frames offset end
                while (lineIndex < linesCount)
                {
                    string str = GetCurrentLine();

                    if (str == DelimeterString)
                    {
                        foundEnd = true;
                        endIndex = lineIndex;
                        break;
                    }

                    ++lineIndex;
                }

                if (foundEnd)
                {
                    if (startIndex > 0 && endIndex > 0 && startIndex <= endIndex)
                    {
                        frameLines = new List<string>();
                        for (int j = startIndex; j < endIndex; ++j)
                        {
                            frameLines.Add(lines[j].TrimStart(new char[] { '#', ' ' }));
                        }
                    }
                }
            }

            return frameLines;
        }
        /// <summary>
        /// Attempts to convert string representation of tracks frame offset into numerical values
        /// </summary>
        /// <param name="frameLines"></param>
        /// <returns>Array of numeric tracks frame offsets if succeded, null-reference otherwise</returns>
        protected long[] ExtractFrameOffsets(List<string> frameLines)
        {
            long[] result = null;

            try
            {
                if (frameLines != null && frameLines.Count > 0)
                {
                    result = new long[frameLines.Count];

                    for (int i = 0; i < frameLines.Count; ++i)
                    {
                        result[i] = Convert.ToInt64(frameLines[i]);
                    }
                }
            }
            catch
            {
                result = null;
            }

            return result;
        }
        /// <summary>
        /// Attempts to parse disk frame length
        /// </summary>
        /// <param name="str"></param>
        /// <returns>Disk frame length ( > 0 ) if succeded, -1 otherwise</returns>
        protected long ParseDiscLength(string str)
        {
            long result = -1;

            if (!String.IsNullOrEmpty(str))
            {
                try
                {
                    string[] words = str.Split(' ');

                    result = Convert.ToInt64(words[DiscLenIndex]);
                }
                catch { }
            }

            return result;
        }
        /// <summary>
        /// Attempts to extract prefix from string
        /// </summary>
        /// <param name="prefix"></param>
        /// <returns>Non-null value if succeded, null-reference otherwise</returns>
        protected string ExtractPrefixValue(string prefix)
        {
            string result = null;

            try
            {
                string buf = null;
                bool found = false;

                int remember_line_index = lineIndex;

                while (lineIndex < linesCount)
                {
                    buf = GetCurrentLine();

                    if (buf.StartsWith(prefix))
                    {
                        found = true;
                        break;
                    }
                    ++lineIndex;
                }

                if (found)
                {
                    result = buf.Replace(prefix, "");
                }
                else
                {
                    lineIndex = remember_line_index;
                }
            }
            catch { }

            return result;
        }
        /// <summary>
        /// Attempts to extract artist name and album name from string
        /// </summary>
        /// <param name="str"></param>
        /// <param name="artist_name"></param>
        /// <param name="album_name"></param>
        /// <returns>True if succeded, false otherwise</returns>
        protected bool ExtractTitle(string str, out string artist_name, out string album_name)
        {
            bool result = false;

            artist_name = null;
            album_name = null;

            if (!String.IsNullOrEmpty(str))
            {
                try
                {
                    // str="ArtistName / AlbumName"

                    string[] words = str.Split(new string[] { " / " }, StringSplitOptions.RemoveEmptyEntries);

                    if (words != null && words.Length == 2)
                    {
                        artist_name = words[0];
                        album_name = words[1];

                        result = true;
                    }
                }
                catch { }
            }
            

            return result;
        }
        /// <summary>
        /// Attempts to extract album year from string
        /// </summary>
        /// <param name="str"></param>
        /// <param name="album_year"></param>
        /// <returns>True if succeded, false otherwise</returns>
        protected bool ExtractYear(string str, out int album_year)
        {
            bool result = false;
            album_year = -1;

            try
            {
                album_year = Convert.ToInt32(str);
                result = true;
            }
            catch { }

            return result;
        }
    }
}
